import { ArrayUtils } from '@amcax/base';

type Handler = (_: any) => any;

export class EventGroup {
  static allEvent = '__allevent';

  private readonly _dispatcher: EventDispatcher;
  private readonly _name: string;
  private readonly _hooks: Handler[] = [];

  constructor(name: string, dispatcher: EventDispatcher) {
    this._name = name;
    this._dispatcher = dispatcher;
  }

  get name() {
    return this._name;
  }

  // handler hook in EventGroup will be called before handlers
  // generally used as middleware
  useHook(h: Handler) {
    this._hooks.push(h);
  }

  protected makeEventName(event: string) {
    return this._name + '::' + event;
  }

  // setup event handler in group
  handle(event: string, h: Handler) {
    event = event === '' ? EventGroup.allEvent : event;
    return this._dispatcher.handle(this.makeEventName(event), h);
  }

  async dispatch(data: any) {
    // dispatch to hooks
    for (const h of this._hooks) {
      await h(data);
    }

    // dispatch to handlers which interest in the whole group event
    this._dispatcher.dispatch(this.makeEventName(EventGroup.allEvent), data);

    // dispatch to group event handlers
    for (const [event, eventData] of Object.entries(data)) {
      this._dispatcher.dispatch(this.makeEventName(event), eventData);
    }
  }
}

export class EventDispatcher {
  private static _dispatcher: EventDispatcher;
  private readonly _handlers: Map<string, Handler[]>;
  private readonly _groups: EventGroup[] = [];

  private constructor() {
    this._handlers = new Map<string, Handler[]>();
  }

  static get(): EventDispatcher {
    if (EventDispatcher._dispatcher === undefined) {
      EventDispatcher._dispatcher = new EventDispatcher();
    }

    return EventDispatcher._dispatcher;
  }

  dispatch(event: string, data: any) {
    const handlers = this._handlers.get(event);
    if (!handlers) {
      return;
    }

    handlers.forEach((h) => h(data));
  }

  protected removeHandler(event: string, handler: Handler) {
    const handlers = this._handlers.get(event);
    if (handlers) {
      ArrayUtils.fastRemove(handlers, handler);
    }
  }

  // setup event handler
  handle(event: string, handler: Handler) {
    let handlers = this._handlers.get(event);
    if (!handlers) {
      handlers = [];
      this._handlers.set(event, handlers);
    }

    handlers.push(handler);

    return () => this.removeHandler(event, handler);
  }

  group(groupName: string): EventGroup {
    let group = this._groups.find((group) => group.name === groupName);
    if (!group) {
      group = new EventGroup(groupName, this);
      this._groups.push(group);
      this.handle(groupName, (data) => {
        group && group.dispatch(data);
      });
    }

    return group;
  }
}

export function getEventDispater(): EventDispatcher {
  return EventDispatcher.get();
}
